/*
 * ringtonemaker.c
 * Makes morse code ringtones
 * Created 20120510
 * Last modified 20120511
 */

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <math.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <unistd.h>

#include "morse.h"
#include "ringtonemaker.h"

/* Magic */
int main(int argc, char **argv) {
    /* Variables */
    char *codestr = NULL; /* String to encode */
    char *fname = NULL; /* File name */
    struct wav_header header; /* Wav header */
    int nel; /* Number of elements in file */
    int fd; /* File descriptor */
    
    /* Set the name of the executable */
    exname = argv[0];
    
    /* Populate optstruct */
    parse_opts(argc, argv);
    
    /* Populate non-changing header fields */
    load_header(&header);
    header.data.cksize = 0;
    header.cksize = 0;

    /* Make sure we actually have a something to encode */
    if (optind == argc) {
        usage();
        fprintf(stderr, "\nNo string specified.\n");
        exit(EX_DATAERR);
    }
    /* Get a string to turn into code.  Also, the file name */
    codestr = getcodestr(argc, argv, &fname);
    
    /* Calculate a couple of variables */
    optstruct.ditlen = 1200./optstruct.cspeed;
    optstruct.amplitude = optstruct.loudness / 100.;
    
    /* Make a dit, a dah, and some spaces */
    dit = make_element(1, &ndit);
    dah = make_element(3, &ndah);
    elspace = make_space(1, &nelspace);
    letterspace = make_space(3, &nletterspace);
    wordspace = make_space(7, &nwordspace);
    
    /* Open the output file */
    if ((fd = open(fname, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) < 0) {
        fprintf(stderr, "Unable to open %s\n", fname);
        perror("open");
        return -1;
    }
        
    /* Write the data to the file */
    nel = write_morse(codestr, fd);
    
    /* Update the header */
    update_header_with_nsamples(nel, &header);
    
    /* Print a header */
    if (lseek(fd, 0, SEEK_SET) == -1) {
        fprintf(stderr, "Unbale to seek to beginning of header.\n");
        perror("lseek");
        exit(EX_IOERR);
    }
    if (write(fd, (void *)&header, sizeof(struct wav_header)) == -1) {
        fprintf(stderr, "Unable to write header to file.\n");
        perror("write");
        exit(EX_IOERR);
    }
    close(fd);
    return 0;
}

/* Parse options */
/* Populates optstruct */
int parse_opts(int argc, char **argv) {    
    int ch;
    
    /* Set sensible defaults.  See usage() */
    optstruct.afreq = 440;
    optstruct.loudness = 100;
    optstruct.srate = 44100;
    optstruct.ssize = 2;
    optstruct.cspeed = 20;
    optstruct.fprefix = NULL;
    optstruct.addspace = 0;
    optstruct.verbose = 0;
    
    /* Struct to pass to getopt_long */
    /* If this is updated, don't forget to update usage() */
    static struct option longopts[] = {
        { "audiofreq",  required_argument,  NULL,   'a' },
        { "loudness",   required_argument,  NULL,   'l' },
        { "samplerate", required_argument,  NULL,   'r' },
        { "samplesize", required_argument,  NULL,   's' },
        { "codespeed",  required_argument,  NULL,   'c' },
        { "fileprefix", required_argument,  NULL,   'f' },
        { "addspace",   no_argument,        NULL,   'e' },
        { "verbose",    no_argument,        NULL,   'v' },
        { "help",       optional_argument,  NULL,   'h' },
        { NULL,         0,                  NULL,   0   }
    };
    static char *optstring = "a:l:r:s:c:f:evh?";
    
    /* Parse the options */
    while ((ch = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) {
        switch (ch) {
            case 'a':
                if (parseintopt(optarg, &optstruct.afreq, "Error in audio frequency.") != 0) {
                    exit(EX_USAGE);
                }
                if (optstruct.afreq < 0) {
                    fprintf(stderr, "Invalid audio frequency.\n");
                    exit(EX_DATAERR);
                }
                break;
            case 'l':
                if (parseintopt(optarg, &optstruct.loudness, "Error in loudness.") != 0) {
                    exit(EX_USAGE);
                }
                if (optstruct.loudness < 0 || optstruct.loudness > 100) {
                    fprintf(stderr, "Invalid loudness.\n");
                    exit(EX_DATAERR);
                }
                break;
            case 'r':
                if (parseintopt(optarg, &optstruct.srate, "Error in sample rate.") != 0) {
                    exit(EX_USAGE);
                }
                if (optstruct.srate <= 0) {
                    fprintf(stderr, "Invalid sample rate.\n");
                    exit(EX_DATAERR);
                }
                break;
            case 's':
                if (parseintopt(optarg, &optstruct.ssize, "Error in sample size.") != 0) {
                    exit(EX_USAGE);
                }
                if (!(optstruct.ssize == 2 || optstruct.ssize == 2)) {
                    fprintf(stderr, "Invalid sample size.\n");
                    exit(EX_DATAERR);
                }
                break;
            case 'c':
                if (parseintopt(optarg, &optstruct.cspeed, "Error in code speed.") != 0) {
                    exit(EX_USAGE);
                }
                if (optstruct.cspeed <= 0) {
                    fprintf(stderr, "Invalid code speed.\n");
                    exit(EX_DATAERR);
                }
                break;
            case 'f':
                if (strlen(optarg) > 0) {
                    optstruct.fprefix = malloc(strlen(optarg) + 1);
                    strlcpy(optstruct.fprefix, optarg, sizeof(optstruct.fprefix) + 1);
                }
                break;
            case 'e':
                optstruct.addspace = 1;
                break;
            case 'v':
                optstruct.verbose = 1;
                break;
            case '?':
            case 'h':
            case ':':
            default:
                usage();
                exit(EX_OK);
                break;
        }
    }
           
    return 0;
}

/* Take a string, convert it to an int, and put it in a variable.  Print an error message on error. */
/* Arguments:
 *      intasstr: Integer as string
 *      dst: Pointer to an int into which to put intasstr as an int
 *      errmsg: Error message to print on error
 */
int parseintopt(char *intasstr, int *dst, const char *errmsg) {
    /* Pointer to last character */
    char *endptr;
    errno = 0;
    *dst = strtol(intasstr, &endptr, 0);
    if ((*dst == 0 && errno != 0) || (intasstr == endptr)) { /* Error */
        usage();
        fprintf(stderr, "\n%s\n", errmsg);
        perror("strtol");
        return -1;
    } else {
        return 0;
    }
}
/* Print a usage statement */
/* Arguments:
 *      exname: Name of the executable (argv[0])
 */
void usage() {
    fprintf(stderr, "Generate a .wav file containing a string in morse code\n");
    fprintf(stderr, "\n");
    fprintf(stderr, "usage: %s [options] string\n", exname);
    fprintf(stderr, "\n");
    fprintf(stderr, "    Options (with defaults):\n");
    fprintf(stderr, "        -a, --audiofreq=440     Set the audio frequency.\n");
    fprintf(stderr, "        -l, --loudness=100      Loudness of generated code, as a percent.\n");
    fprintf(stderr, "        -r, --samplerate=44100  Set the sample rate.\n");
/*    fprintf(stderr, "        -s, --samplesize=2      Set the number of bytes per sample.\n");
    fprintf(stderr, "                                    Only 2 is supported.\n");*/
    fprintf(stderr, "        -c, --codespeed=20      Set the speed of the generated morse code.\n");
/*    fprintf(stderr, "        -f, --fileprefix=\"\"     Prefix the generated file name with\n");
    fprintf(stderr, "                                    the specified string.\n");*/
    fprintf(stderr, "        -e, --addspace          Add a morse code space to the end of the\n");
    fprintf(stderr, "                                    generated morse code.\n");
/*    fprintf(stderr, "        -v  --verbose           Print verbose output.\n"); */
    fprintf(stderr, "        -h, --help              Show this help.\n");
}

/* Print config options */
void print_config() {
    printf("Config:\n");
    printf("\tAudio Frequency:    %i\n", optstruct.afreq);
    printf("\tLoudness:           %i\n", optstruct.loudness);
    printf("\tSample Rate:        %i\n", optstruct.srate);
    printf("\tSample Size:        %i\n", optstruct.ssize);
    printf("\tCode Speed:         %i\n", optstruct.cspeed);
    printf("\tFile Prefix:        %s\n", optstruct.fprefix);
    printf("\tAdd Space:          %s\n", optstruct.addspace?"True":"False");
    printf("\tVerbose:            %s\n", optstruct.verbose?"True":"False");
}

/* Print a TODO message */
void TODO(char *todostring) {
    fprintf(stderr, "%s\n", todostring);
}

/* Load the header.  optstruct *must* be populated before this is called. */
/* Arguments:
 *      header: The header to populate
 */
void load_header(struct wav_header *header) {
    memset(header, 0, sizeof(struct wav_header));
    /* Set non-changing parameters */
    strncpy((char*) &header->ckID, "RIFF", sizeof(uint32_t));
    strncpy((char*) &header->WAVEID, "WAVE", sizeof(uint32_t));
    strncpy((char*) &header->fmt.ckID, "fmt ", sizeof(uint32_t));
    header->fmt.cksize = 16;
    header->fmt.wFormatTag = WAVE_FORMAT_PCM;
    strncpy((char*) &header->data.ckID, "data", sizeof(uint32_t));
    
    /* From Config */
    header->fmt.nChannels = NCHANNELS;
    header->fmt.nSamplesPerSec = optstruct.srate;
    header->fmt.nBlockAlign = optstruct.ssize * NCHANNELS;
    header->fmt.nAvgBytesPerSec = header->fmt.nBlockAlign * optstruct.srate;
    header->fmt.wBitsPerSample = optstruct.ssize * 8;
}

/* Update the header with a known number of samples */
/* Arguments:
 *      nsamples: Number of samples in the file.  This should be even.
 *      header: The header to update.
 */
void update_header_with_nsamples(int nsamples, struct wav_header *header) {
    header->data.cksize = header->fmt.nBlockAlign * NCHANNELS * nsamples;
    header->cksize = sizeof(header->WAVEID) + sizeof(header->fmt) + sizeof(header->data) + header->data.cksize;
    /* God forbid this is odd */
    if (header->cksize & (int)0x01)
        fprintf(stderr, "WARNING: Odd number of data bytes.\n");
        header->cksize++;
}

/* Get the string to turn into code.  Returns allocated memory */
char* getcodestr(int argc, char **argv, char **fname) {
    int i, size = 0;
    size_t ret, namesize;
    char *codestr = NULL;
    /* Calculate size of string */
    for (i = optind; i < argc; i++) {
        size += strlen(argv[i]) + 1;
    }
    /* Add space for a final space if necessary */
    if (optstruct.addspace) {
        size += 1;
    }
    /* Allocate and zero memory for our string */
    codestr = malloc(size);
    memset(codestr, 0, size);
    /* Add each word to the string */
    for (i = optind; i < argc; i++) {
        if (strlcat(codestr, argv[i], size) >= size) {
            fprintf(stderr, "Error in building string to encode.\n");
            exit(EX_SOFTWARE);
        }
        /* Don't add a space if it's the last word */
        if ((i + 1) < argc && strlcat(codestr, " ", size) >= size) {
            fprintf(stderr, "Error in building string to encode.\n");
            exit(EX_SOFTWARE);
        }
    }
    /* Make a file name */
    namesize = strlen(codestr) + strlen(".wav") + 1;
    *fname = malloc(namesize);
    memset(*fname, 0, namesize);
    if (strlcpy(*fname, codestr, namesize) >= namesize) {
        fprintf(stderr, "Error in building file name.\n");
        exit(EX_SOFTWARE);
    }
    if (strlcat(*fname, ".wav", namesize) >= namesize) {
        fprintf(stderr, "Error in terminating file name.\n");
        exit(EX_SOFTWARE);
    }
    /* Add a terminating space if necessary */
    if (optstruct.addspace) {
        if (strlcat(codestr, " ", size) >= size) {
            fprintf(stderr, "Error in building string to encode.\n");
            exit(EX_SOFTWARE);
        }
    }

    return codestr;
}

/* Make a code element */
/* Arguments:
 *      len: number of dits long the element is to be
 *      count: stores number of elements
 */
int16_t* make_element(int len, int *count) {
    int i;
    /* Number of samples in the element */
    int nsamples = (int)(RISETIME + len * optstruct.ditlen) * (optstruct.srate / 1000.);
    /* Allocate and zero memory for the element */
    float *el = malloc(nsamples * sizeof(float));
    memset(el, 0, nsamples * sizeof(float));
    /* The array to return */
    void *retarray = malloc(nsamples * optstruct.ssize);
    /* The whole thing is a giant sine wave */
    for (i = 0; i < nsamples; i++)
        el[i] = sin(2 * M_PI * optstruct.afreq * i / optstruct.srate);
    /* Shape the beginning and end */
    int nrise = optstruct.srate / 1000 * RISETIME; /* Number of samples to shape */
    for (i = 0; i < nrise; i++)
        el[i] = ((sin(((float)i / (float)nrise * M_PI) - (M_PI / 2)) + 1) / 2) * el[i];
    for (i = nsamples - nrise; i < nsamples; i++)
        el[i] = ((sin(((float)i / (float)nrise * M_PI) + (M_PI / 2)) + 1) / 2) * el[i];
    /* Put it in an array of the appropriate type */
    int16_t *tmparray = malloc(nsamples * sizeof(int16_t));
    switch (optstruct.ssize) {
//        case 1:
//            for (i = 0; i < nsamples; i++) {
//               // ((uint8_t*)retarray)[i] = (uint8_t)el[i];
//                ;
//            }
        case 2:
            for (i = 0; i < nsamples; i++) {
                tmparray[i] = (int16_t)(el[i] * INT16_MAX * optstruct.amplitude);
            }
            break;
    }
    *count = nsamples;
    return tmparray;
}

/* Makes spaces */
int16_t* make_space(int len, int *count) {
    int size = (len * optstruct.ditlen - RISETIME) * (optstruct.srate / 1000.);
    int16_t *space = malloc(size * sizeof(uint16_t));
    memset(space, 0, size * sizeof(uint16_t));
    *count = size;
    return space;
}

/* Print the header */
/* Arguments
 *      header: the header
 */
void print_header(struct wav_header *header) {
    printf("Number of samples: %lu\n", (header->cksize - 4 - sizeof(header->fmt) - sizeof(header->data)) / header->fmt.nBlockAlign);
}

/* Write morse to the file. */
/* Arguments
 *      str: The string to encode
 *      fd: The file descriptor to which to write.
 */
int write_morse(char *str, int fd) {
    int i, j;
    char *morse;
    int16_t *el; /* Element to output */
    int nel; /* Number of frames in el */
    int totalframes = 0; /* Total frames written */
    
    /* Position the file pointer to right after the header */
    if (lseek(fd, sizeof(struct wav_header), SEEK_SET) == -1) {
        fprintf(stderr, "Unable to seek to beginning of data.\n");
        perror("lseek");
        exit(EX_IOERR);
    }
    
    /* Iterate through input string */
    for (i = 0; i < strlen(str); i++) {
        /* Output morse if we have it. */
        if (morse_table[str[i]].morse != NULL) {
            /* Put a space between letters */
            if (i > 0 && morse_table[str[i-1]].morse != NULL) {
                if (write(fd, letterspace, nletterspace * sizeof(int16_t)) == -1) {
                    fprintf(stderr, "Unbale to write interletter space.\n");
                    perror("write");
                    exit(EX_IOERR);
                }
                totalframes += nletterspace;
            }
            /* Save typing */
            morse = morse_table[str[i]].morse;
            /* Loop through elements in the character */
            for (j = 0; j < strlen(morse); j++) {
                /* Choose dit or dah */
                if (morse[j] == '1') {
                    el = dit;
                    nel = ndit;
                } else if (morse[j] == '3') {
                    el = dah;
                    nel = ndah;
                } else {
                    fprintf(stderr, "Error in morse table at index %i.\n", str[i]);
                    exit(EX_SOFTWARE);
                }
                /* Put a space between elements */
                if (j > 0) { 
                    if (write(fd, elspace, nelspace * sizeof(int16_t)) == -1) {
                        fprintf(stderr, "Unbale to write interletter space.\n");
                        perror("write");
                        exit(EX_IOERR);
                    }
                    totalframes += nelspace;
                }
                /* Write the element */
                if (write(fd, el, nel * sizeof(int16_t)) == -1) {
                    fprintf(stderr, "Unbale to write interletter space.\n");
                    perror("write");
                    exit(EX_IOERR);
                }
                totalframes += nel;
            }
        /* Output a space */
        } else if (isspace(str[i])) {
            if (write(fd, wordspace, nwordspace * sizeof(int16_t)) == -1) {
                fprintf(stderr, "Unbale to write interletter space.\n");
                perror("write");
                exit(EX_IOERR);
            }
            totalframes += nwordspace;
        /* Don't bother with unprintable characters */
        } else if (!isprint(str[i])) {
            fprintf(stderr, "WARNING: ignoring non-printable character 0x%02X.  Ignoring.\n", str[i]);
        /* Extended ASCII? We don't serve their kind here. */
        } else if (str[i] > sizeof(morse_table) / sizeof(struct morse_table_entry)) {
            fprintf(stderr, "WARNING: Ignoring extended ASCII character 0x%02X.\n", str[i]);
        /* Woah */
        } else {
            fprintf(stderr, "Something went wrong translating to morse.\n");
            exit(EX_SOFTWARE);
        }
    }
    return totalframes;
}